Skip to content

fix(desktop): explain the pairing 404 instead of surfacing a bare transport error - #5340

Open
akeemjenkins wants to merge 1 commit into
block:mainfrom
akeemjenkins:fix/pairing-404-actionable-error
Open

fix(desktop): explain the pairing 404 instead of surfacing a bare transport error#5340
akeemjenkins wants to merge 1 commit into
block:mainfrom
akeemjenkins:fix/pairing-404-actionable-error

Conversation

@akeemjenkins

Copy link
Copy Markdown

Closes #3779.

Duplicate check: none found for this change. #3779 has no PR attached. The related compose-side work (#2736, #3627, #3875, #4656) and the chart routing (#3424) fix the deployments; this fixes the message for every deployment they cannot reach — external reverse proxies, the Railway template mentioned in #3779, Helm installs without pairingRelay, and anything hand-rolled.

Problem

When a NIP-43 relay advertises no pairing_relay_url, pairing_relay_from_nip11 returns LegacyPath and resolve_pairing_relay_url appends /pair to the relay's own host. That address is chosen from an advertisement and never probed, so when nothing serves it the QR panel showed only:

WebSocket connection failed: HTTP error: 404 Not Found

No URL, no route decision, no config lever. Per #3779 and its confirmations, self-hosters had to read config.rs and pairing.rs to get unstuck.

Change

The route was already a typed enum but collapsed to a String before the task that reports failures could see it. PairingRelay is now Clone and travels into PairingTaskContext, so describe_connect_failure can explain the failure in terms of the decision that produced the URL.

A 404 on the legacy branch:

Pairing endpoint wss://relay.example.com/pair not found (404). This relay
advertises NIP-43 but no pairing_relay_url, so pairing fell back to
wss://relay.example.com/pair, and nothing is serving that path. The relay
operator needs to set BUZZ_PAIRING_RELAY_URL on the relay, or route /pair to a
buzz-pair-relay instance (see deploy/compose/README.md, "Device pairing").

Everything else keeps the original transport error and gains the attempted URL — the shape #3779 suggested, and what the split-domain case needs when a configured pairing_relay_url is simply wrong.

One deliberate narrowing: the actionable text is gated on 404 on the legacy branch specifically. A routed /pair whose sidecar is down answers 502, which is a different problem with a different fix, so it must not be told nothing is serving the path. There is a test for that.

Tests

Four added to pairing_relay_tests.rs, beside the existing route tests:

  • legacy_path_404_explains_the_missing_pairing_endpoint — asserts the URL, the NIP-43 reason, and both levers appear
  • legacy_path_non_404_keeps_the_transport_error_and_gains_the_url — the 502 case above
  • configured_pairing_relay_404_reports_the_url_it_was_given — split domain; no /pair advice, which would point at the wrong file
  • main_relay_failure_keeps_the_transport_error

cargo test -p buzz-desktop: 2274 passed, 0 failed. cargo fmt --check and cargo clippy --all-targets clean.

Why this one matters beyond the compose fix

We run a small managed Buzz fleet and hit this exact 404 on our own tenants (details in #2736). We had the source open, so we found it in an afternoon. The message is what everyone else has to work from, and it is the only part of this that helps a relay whose misconfiguration no upstream default can prevent.

The wording points at deploy/compose/README.md. If #2736 or a sibling lands and renames that section, this string should follow it — happy to rebase onto whichever lands first.

…nsport error

Closes block#3779.

When a NIP-43 relay advertises no pairing_relay_url, the desktop falls
back to appending /pair to the relay's own host. If nothing serves that
path the QR panel showed only:

    WebSocket connection failed: HTTP error: 404 Not Found

which names neither the URL that was dialled, nor the branch that chose
it, nor either lever that fixes it. Self-hosters on block#2734 had to read
config.rs and pairing.rs to get unstuck.

The route is already a typed enum, but it collapsed to a String before
the task that reports the failure could see it. PairingRelay is now
Clone and travels into PairingTaskContext, so the connect error can be
described in terms of the decision that produced the URL.

A 404 on the legacy branch now names the URL, says the relay advertises
NIP-43 without a pairing_relay_url, and points at both fixes
(BUZZ_PAIRING_RELAY_URL, or routing /pair to a buzz-pair-relay).

Every other failure keeps the original transport error and gains the
attempted URL, which is what the split-domain case needs when a
configured pairing_relay_url is simply wrong. A 502 from a routed /pair
whose sidecar is down must not claim nothing is serving the path, and a
test pins that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Akeem Jenkins <accounts@akeemjenkins.com>
@akeemjenkins
akeemjenkins requested a review from a team as a code owner August 8, 2026 17:34
@kiranmagic7

Copy link
Copy Markdown
Contributor

@akeemjenkins, this overlaps #3846, which has been open for #3779 since July 31 and already separates the legacy /pair case from configured pairing_relay_url failures. One material difference: describe_connect_failure interpolates url directly, so a valid configured URL such as wss://pair.example/connect?token=secret#fragment is copied into the UI error. #3846 strips the query and fragment before display and pins that behavior in both legacy and configured-route tests. Whichever patch lands should keep that redaction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(desktop): pairing shows a bare 404 when the legacy /pair fallback hits a relay with no pairing endpoint

2 participants